<?php

/**
 * Louder Cache
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file docs/LICENSE.txt.
 *
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to info@loudertechnology.com so we can send you a copy immediately.
 *
 * @category Louder
 * @package CacheStorage
 * @subpackage Adapters
 * @copyright Copyright (c) 2005-2009 Andres Felipe Gutierrez (gutierrezandresfelipe at gmail.com)
 * @license New BSD License
 */

/**
 * Adaptador de almacenamiento para Memcache
 *
 * @category Louder
 * @package CacheStorage
 * @subpackage Adapters
 * @copyright Copyright (c) 2005-2009 Andres Felipe Gutierrez (gutierrezandresfelipe at gmail.com)
 * @license New BSD License
 */
class MemcacheCacheStorage implements CacheStorageInterface {

	/**
	 * Objeto Memcache
	 *
	 * @var Memcache
	 */
	private $_handler;

	/**
	 * Arbol temporal
	 *
	 * @var array
	 */
	private $_data = array();

	/**
	 * Codigo Autonumerico para la creacion de nodos
	 *
	 * @var node
	 */
	private $_id = 1;

	/**
	 * Indice del arbol por codigo de nodo
	 *
	 * @var array
	 */
	private $_indexNode = array();

	/**
	 * Indice del arbol por nivel
	 *
	 * @var array
	 */
	private $_indexLevel = array();

	/**
	 * Atributo temporal
	 *
	 * @var string
	 */
	private $_attribute = "";

	/**
	 * Indice del arbol por llave
	 *
	 * @var array
	 */
	private $_indexKey = array();

	/**
	 * Constructor de MemcacheCacheStorage
	 *
	 * @access public
	 */
	public function __construct($settings=array()){
		if(!extension_loaded('memcache')){
			throw new CacheStorageException("La extensión php_memcache es requerida por CacheStorage");
		}
		if(!isset($settings['server'])){
			$settings['server'] = "127.0.0.1";
		}
		if(!isset($settings['port'])){
			$settings['port'] = 11211;
		}
		$this->_handler = new Memcache();
		$this->_handler->pconnect($settings['server'], $settings['port']);
	}

	/**
	 * Verifica si existe un nodo en el arbol
	 *
	 * @param string $index
	 */
	public function isSetIndex($path){
		if($this->_handler->get('data')===false){
			return false;
		} else {
			return true;
		}
	}

	/**
	 * Obtiene un modo por su path
	 *
	 * @param string $path
	 * @return mixed
	 */
	public function getNodeByPath($path){
		$this->_retrieveData();
		$elements = $this->_getAsArray($path);
		$ce = count($elements);
		$level = 0;
		$node_id = 0;
		$leaf = null;
		for($i=0;$i<$ce;$i++){
			$found = false;
			if(isset($this->_indexKey[$elements[$i]])){
				foreach($this->_indexKey[$elements[$i]] as $id){
					$row = &$this->_data[$id];
					if($row['level']==$level){
						$found = true;
						$node_id = $row['id'];
						$level = $row['level']+1;
						$leaf = &$row;
						break;
					}
				}
			}
			if($found==false){
				return null;
			}
		}
		return $leaf;
	}

	/**
	 * Convierte un PATH de cadena de caracteres a Array
	 *
	 * @param string $str
	 * @return array
	 */
	private function _getAsArray($str){
		if($str!='/'){
			$length = strlen($str);
			$elements = array();
			$element = "";
			$oldchar = "";
			$j = 0;
			for($i=0;$i<$length;$i++){
				$char = substr($str, $i, 1);
				if($char!='/'&&$oldchar!="\\"){
					$element.=$char;
				} else {
					if($j==0){
						if($element!=""){
							$elements[] = $element;
						}
					} else {
						$elements[] = $element;
					}
					$element = "";
					$j++;
				}
			}
			if($j==0){
				if($element!=""){
					$elements[] = $element;
				}
			} else {
				$elements[] = $element;
			}
		} else {
			$elements = array();
		}
		return $elements;
	}

	/**
	 * Agrega un atributo a un nodo
	 *
	 * @param int $id
	 * @param string $index
	 * @param mixed $value
	 * @return boolean
	 */
	public function addNodeAttribute($id, $index, $value){
		$this->_retrieveAttributeData($id, $index);
		if(isset($this->_data[$id])){
			$this->_attribute = $value;
		} else {
			return false;
		}
		$this->_storeAttributeData($id, $index);
		return true;
	}

	/**
	 * Obtiene el valor de un atributo
	 *
	 * @param int $id
	 * @param string $index
	 * @return mixed
	 */
	public function getNodeAttribute($id, $index){
		$this->_retrieveAttributeData($id, $index);
		if(isset($this->_data[$id])){
			return $this->_attribute;
		} else {
			return null;
		}
	}

	/**
	 * Obtiene todos los datos del memcached para trabajar con atributos
	 *
	 * @access private
	 */
	private function _retrieveAttributeData($id, $index){
		$this->_data = $this->_handler->get('data');
		if(is_bool($this->_data)){
			$this->_data = array();
		}
		$indexName = "$id?$index";
		$this->_attribute = $this->_handler->get($indexName);
		if(is_bool($this->_attribute)){
			$this->_attribute = "";
		}
	}

	/**
	 * Almacena el atributo
	 *
	 * @access private
	 */
	private function _storeAttributeData($id, $index){
		$indexName = "$id?$index";
		$this->_handler->set($indexName, $this->_attribute);
	}

	/**
	 * Obtiene todos los datos del memcached para efectuar la operacion
	 *
	 * @access private
	 */
	private function _retrieveData(){
		$this->_data = $this->_handler->get('data');
		if(is_bool($this->_data)){
			$this->_data = array();
		}
		$this->_indexKey = $this->_handler->get('ikey');
		if(is_bool($this->_indexKey)){
			$this->_indexKey = array();
		}
		$this->_indexLevel = $this->_handler->get('ilev');
		if(is_bool($this->_indexLevel)){
			$this->_indexLevel = array();
		}
		$this->_indexNode = $this->_handler->get('inod');
		if(is_bool($this->_indexNode)){
			$this->_indexNode = array();
		}
		$this->_id = $this->_handler->get('id');
		if(is_bool($this->_id)){
			$this->_id = 0;
		}
	}

	/**
	 * Obtiene todos los datos del memcached para efectuar la operacion
	 *
	 * @access private
	 */
	private function _storeData(){
		$this->_handler->set('data', $this->_data);
		$this->_handler->set('ikey', $this->_indexKey);
		$this->_handler->set('ilev', $this->_indexLevel);
		$this->_handler->set('inod', $this->_indexNode);
		$this->_handler->set('id', $this->_id);
	}

	/**
	 * Crea un nodo en el arbol
	 *
	 * @param int $parent
	 * @param int $level
	 * @param mixed $key
	 * @return array
	 */
	private function _addNode($parent, $level, $key){
		$this->_id++;
		$this->_data[$this->_id] = array(
			'id' => $this->_id,
			'node_id' => $parent,
			'level' => $level,
			'key' => $key
		);
		if(!isset($this->_indexKey[$key])){
			$this->_indexKey[$key] = array();
		}
		$this->_indexKey[$key][] = $this->_id;
		if(!isset($this->_indexNode[$parent])){
			$this->_indexNode[$parent] = array();
		}
		$this->_indexNode[$parent][] = $this->_id;
		if(!isset($this->_indexLevel[$level])){
			$this->_indexLevel[$level] = array();
		}
		$this->_indexLevel[$level][] = $this->_id;
		return $this->_id;
	}

	/**
	 * Agrega nodos a otro nodo
	 *
	 * @param string $path
	 * @param array $items
	 */
	public function addChild($path, $items){
		$this->_retrieveData();
		$elements = $this->_getAsArray($path);
		$ce = count($elements);
		$level = 0;
		$node_id = 0;
		$leaf = null;
		for($i=0;$i<$ce;$i++){
			$found = false;
			if(isset($this->_indexKey[$elements[$i]])){
				foreach($this->_indexKey[$elements[$i]] as $id){
					$row = &$this->_data[$id];
					if($row['level']==$level){
						$found = true;
						$node_id = $row['id'];
						$level = $row['level']+1;
						$leaf = &$row;
						break;
					}
				}
			}
			if($found==false){
				$node_id = $this->_addNode($node_id, $level, $elements[$i]);
				$leaf = &$this->_data[$node_id];
				$level++;
			}
		}
		$node_id = $leaf['id'];
		$level = $leaf['level']+1;
		$ci = count($items);
		for($i=0;$i<$ci;$i++){
			$found = false;
			if(isset($this->_indexKey[$items[$i]])){
				foreach($this->_indexKey[$items[$i]] as $id){
					$row = &$this->_data[$id];
					if($row['level']==$level){
						$found = true;
						$node_id = $row['id'];
						$level = $row['level']+1;
						$leaf = &$row;
						break;
					}
				}
			}
			if($found==false){
				$node_id = $this->_addNode($node_id, $level, $items[$i]);
				$leaf = &$this->_data[$node_id];
				$level++;
			}
		}
		$this->_storeData();
		return $this->_data[$node_id];
	}


	/**
	 * Agrega nodos a otro nodo por su id
	 *
	 * @param int $id
	 * @param array $items
	 */
	public function addChildById($id, $items){
		$this->_retrieveData();
		if($id>0){
			if(!isset($this->_data[$id])){
				return null;
			}
			$leaf = &$this->_data[$id];
			$node_id = $leaf['id'];
			$level = $leaf['level']+1;
		} else {
			$node_id = 0;
			$level = 0;
		}
		$ci = count($items);
		for($i=0;$i<$ci;$i++){
			$found = false;
			if(isset($this->_indexKey[$items[$i]])){
				foreach($this->_indexKey[$items[$i]] as $id){
					$row = &$this->_data[$id];
					if($row['level']==$level){
						$found = true;
						$node_id = $row['id'];
						$level = $row['level']+1;
						$leaf = &$row;
						break;
					}
				}
			}
			if($found==false){
				$node_id = $this->_addNode($node_id, $level, $items[$i]);
				$leaf = &$this->_data[$node_id];
				$level++;
			}
		}
		$this->_storeData();
		return $this->_data[$node_id];
	}

	function gt(){
		return $this->_handler->get('2?isCartoonCharacter');
		#return $this->_handler->get('data');
	}

	/**
	 * Obtiene el LOCK actual del arbol
	 *
	 * @access public
	 * @return boolean
	 */
	public function getLock(){
		$lock = $this->_handler->get('_lock');
		if($lock===false){
			$this->unlock();
			return false;
		} else {
			return $lock ? true : false;
		}
	}

	/**
	 * Cambia la bandera de bloqueo a LOCK
	 *
	 * @access public
	 */
	public function lock(){
		$this->_handler->set('_lock', 1);
	}

	/**
	 * Cambia la bandera de bloqueo a UNLOCK
	 *
	 * @access public
	 */
	public function unlock(){
		$this->_handler->set('_lock', 0);
	}

}